﻿//-----------------------------------------------------------------------
// <copyright file="ControllerMockeryExtensions.cs" company="OOHM PROCESSAMENTO DE DADOS LTDA.">
//     Microsoft Public License (MS-PL) This license governs use of the accompanying
//     software. If you use the software, you accept this license. If you do not
//     accept the license, do not use the software. 1. Definitions The terms "reproduce,"
//     "reproduction," "derivative works," and "distribution" have the same meaning
//     here as under U.S. copyright law. A "contribution" is the original software,
//     or any additions or changes to the software. A "contributor" is any person
//     that distributes its contribution under this license. "Licensed patents"
//     are a contributor's patent claims that read directly on its contribution.
//     2. Grant of Rights (A) Copyright Grant- Subject to the terms of this license,
//     including the license conditions and limitations in section 3, each contributor
//     grants you a non-exclusive, worldwide, royalty-free copyright license to
//     reproduce its contribution, prepare derivative works of its contribution,
//     and distribute its contribution or any derivative works that you create.
//     (B) Patent Grant- Subject to the terms of this license, including the license
//     conditions and limitations in section 3, each contributor grants you a non-exclusive,
//     worldwide, royalty-free license under its licensed patents to make, have
//     made, use, sell, offer for sale, import, and/or otherwise dispose of its
//     contribution in the software or derivative works of the contribution in
//     the software. 3. Conditions and Limitations (A) No Trademark License- This
//     license does not grant you rights to use any contributors' name, logo, or
//     trademarks. (B) If you bring a patent claim against any contributor over
//     patents that you claim are infringed by the software, your patent license
//     from such contributor to the software ends automatically. (C) If you distribute
//     any portion of the software, you must retain all copyright, patent, trademark,
//     and attribution notices that are present in the software. (D) If you distribute
//     any portion of the software in source code form, you may do so only under
//     this license by including a complete copy of this license with your distribution.
//     If you distribute any portion of the software in compiled or object code
//     form, you may only do so under a license that complies with this license.
//     (E) The software is licensed "as-is." You bear the risk of using it. The
//     contributors give no express warranties, guarantees or conditions. You may
//     have additional consumer rights under your local laws which this license
//     cannot change. To the extent permitted under your local laws, the contributors
//     exclude the implied warranties of merchantability, fitness for a particular
// </copyright>
//-----------------------------------------------------------------------
namespace AbstractSaaSTemplate.Utility.Tests.UI.Utilities{    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Routing;
    using System.Web.SessionState;
    using AbstractSaaSTemplate.UI.Web;
    using AbstractSaaSTemplate.UI.Web.Helpers;
    using AbstractSaaSTemplate.Utility.Tests.UI.Utilities.FakeHttpObjects;
    using Moq;

    /// <summary>
    /// Provides extensions to the controller object that can be used to mock the environment for testing.
    /// </summary>
    public static class ControllerMockeryExtensions
    {
        /// <summary>
        /// Gets or sets the Controller Context cached.
        /// </summary>
        private static ControllerContext ControllerContext { get; set; }

        /// <summary>
        /// Gets or sets the Request Context cached.
        /// </summary>
        private static RequestContext RequestContext { get; set; }

        /// <summary>
        /// Gets or sets the url helper cached.
        /// </summary>
        private static UrlHelper UrlHelper { get; set; }

        /// <summary>
        /// Gets or sets the HttpContext cached.
        /// </summary>
        private static HttpContext TraditionalHttpContext { get; set; }

        /// <summary>
        /// Gets or sets the list of items that should be included to the QueryString.
        /// </summary>
        private static IDictionary<string, string> QueryString { get; set; }

        /// <summary>
        /// Extends the controller with a feature to mock the controller for testing
        /// </summary>
        /// <param name="baseController">The controller which will be extended</param>
        public static void MockForTesting(this Controller baseController)
        {
            MockForTesting(
                baseController,
                string.Empty,
                HttpVerbs.Get);
        }

        /// <summary>
        /// Extends the controller with a feature to mock the controller for testing
        /// </summary>
        /// <param name="baseController">The controller which will be extended</param>
        /// <param name="urlAuthority">The autority used to create the route and context.</param>
        /// <param name="queryString">Items to be loaded to the query string.</param>
        public static void MockForTesting(
            this Controller baseController,
            string urlAuthority,
            IDictionary<string, string> queryString)
        {
            QueryString = queryString;
            
            MockForTesting(
                baseController,
                urlAuthority,
                string.Empty,
                HttpVerbs.Get);
        }
        
        /// <summary>
        /// Extends the controller with a feature to mock the controller for testing
        /// </summary>
        /// <param name="baseController">The controller which will be extended</param>
        /// <param name="verb">The HttpMethod of the request.</param>
        public static void MockForTesting(this Controller baseController, HttpVerbs verb)
        {
            MockForTesting(
                baseController,
                string.Empty,
                verb);
        }

        /// <summary>
        /// Extends the controller with a feature to mock the controller for testing
        /// </summary>
        /// <param name="baseController">The controller which will be extended</param>
        /// <param name="actionName">The action name for which the route will be defined.</param>
        /// <param name="verb">The HttpMethod of the request.</param>
        public static void MockForTesting(
            this Controller baseController,
            string actionName,
            HttpVerbs verb)
        {
            MockForTesting(
                baseController,
                string.Empty,
                string.Empty,
                verb);
        }
        
        /// <summary>
        /// Extends the controller with a feature to mock the controller for testing
        /// </summary>
        /// <param name="baseController">The controller which will be extended</param>
        /// <param name="urlAuthority">The autority used to create the route and context.</param>
        /// <param name="actionName">The action name for which the route will be defined.</param>
        /// <param name="verb">The HttpMethod of the request.</param>
        public static void MockForTesting(
            this Controller baseController,
            string urlAuthority,
            string actionName,
            HttpVerbs verb)
        {
            InitializeCachedContext(
                baseController,
                urlAuthority,
                string.Empty,
                string.Empty,
                actionName,
                verb);

            HttpContext.Current = TraditionalHttpContext;
            baseController.ControllerContext = ControllerContext;
            baseController.Url = UrlHelper;
        }

        /// <summary>
        /// Initializes the cached version of the properties that will be set
        /// to the mocked context.
        /// </summary>
        /// <param name="baseController">The controller which will be extended</param>
        /// <param name="urlAuthority">The autority used to create the route and context.</param>
        /// <param name="areaName">The name of the area.</param>
        /// <param name="controllerName">The name of the controller.</param>
        /// <param name="actionName">The action name for which the route will be defined.</param>
        /// <param name="verb">The HttpMethod of the request.</param>
        private static void InitializeCachedContext(
            Controller baseController,
            string urlAuthority,
            string areaName,
            string controllerName,
            string actionName,
            HttpVerbs verb)
        {
            if (baseController == null)
            {
                throw new ArgumentNullException("baseController");
            }

            if (string.IsNullOrWhiteSpace(urlAuthority))
            {
                urlAuthority = "localhost:8888/";
            }
            
            if (string.IsNullOrWhiteSpace(areaName))
            {
                areaName = "Root";
            }

            if (string.IsNullOrWhiteSpace(controllerName))
            {
                controllerName = baseController
                    .GetType()
                    .Name
                    .Replace("Controller", string.Empty);
            }

            if (string.IsNullOrWhiteSpace(actionName))
            {
                actionName = "Index";
            }

            // prepare the collections used through out the object
            var cookiesCollection = new HttpCookieCollection();
            var serverVariables = new System.Collections.Specialized.NameValueCollection();
            var headers = new System.Collections.Specialized.NameValueCollection();
            var queryString = new System.Collections.Specialized.NameValueCollection();
            var forms = new System.Collections.Specialized.NameValueCollection();

            // load query string items, if that makes any sense
            if (QueryString != null)
            {
                foreach (var item in QueryString)
                {
                    queryString.Add(item.Key, item.Value);
                }
            }

            // prepare the request for this Mock Attempt.
            var routeData = new
            {
                @area = areaName,
                @controller = controllerName,
                @action = actionName,
                @id = UrlParameter.Optional
            };

            // check format of the scheme.
            const string Scheme = "http://";
            if (!urlAuthority.StartsWith(Scheme))
            {
                urlAuthority = string.Concat(
                    Scheme,
                    urlAuthority);
            }

            // defines the basic url for the mocking context
            // if possible, extract this from test configuration parameters
            var baseUrl = string.Concat(
                urlAuthority.TrimEnd('/'),
                "/",
                routeData.controller,
                "/",
                routeData.action);

            if (QueryString != null)
            {
                foreach (var item in QueryString)
                {
                    var concatChar = "&";
                    if (!baseUrl.Contains("?"))
                    {
                        concatChar = "?";
                    }

                    baseUrl = string.Concat(
                        baseUrl,
                        concatChar,
                        item.Key,
                        "=",
                        item.Value);
                }
            }

            // preparing mocks that will be used to generate it
            var requestContext = new Mock<RequestContext>();
            var request = new Mock<HttpRequestBase>(MockBehavior.Strict);
            var response = new Mock<HttpResponseBase>();
            var session = null as MockHttpSession;

            var routes = new RouteCollection();
            routes.MapRoute(
                "Default",
                "{area}/{controller}/{action}/{id}",
                routeData);

            var contextRouteData = new RouteData();
            contextRouteData.Values.Add("area", routeData.area);
            contextRouteData.Values.Add("controller", routeData.controller);
            contextRouteData.Values.Add("action", routeData.action);

            // PART 1 - Generate the assets for the HttpContext
            if (ControllerContext != null)
            {
                session = new MockHttpSession(TraditionalHttpContext.Session);

                // load the original request context and request
                // that will be used to keep everything from the
                // previous mock run over this controller
                var originalRequestContext = RequestContext;
                var originalRequest = ControllerContext.HttpContext.Request;

                // first we setup the request context
                requestContext.SetupGet(x => x.RouteData).Returns(originalRequestContext.RouteData);

                // after that, we setup the request
                request.SetupGet(x => x.HttpMethod).Returns(originalRequest.HttpMethod);
                request.SetupGet(x => x.ApplicationPath).Returns(originalRequest.ApplicationPath);
                request.SetupGet(x => x.FilePath).Returns(originalRequest.FilePath);
                request.SetupGet(x => x.Url).Returns(new Uri(baseUrl, UriKind.RelativeOrAbsolute));
                request.SetupGet(x => x.Headers).Returns(originalRequest.Headers);
                request.SetupGet(x => x.ServerVariables).Returns(originalRequest.ServerVariables);
                request.SetupGet(x => x.QueryString).Returns(originalRequest.QueryString);
                request.SetupGet(x => x.Form).Returns(originalRequest.Form);
                request.SetupGet(x => x.Cookies).Returns(originalRequest.Cookies);
                request.SetupGet(x => x.RequestContext).Returns(originalRequestContext);

                // and we setup the reposnse, including the output
                response.SetupGet(x => x.Cookies).Returns(originalRequest.Cookies);
                response.Setup(x => x.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>((s) => s);
                using (var memoryStream = new MemoryStream())
                {
                    response.SetupGet(x => x.Output).Returns(new StreamWriter(memoryStream));
                }
            }
            else
            {
                // we load the current session from the context
                // that will avoid information such as the logon instance
                session = new MockHttpSession();

                // prepare the request context
                requestContext.SetupGet(x => x.RouteData).Returns(contextRouteData);

                // prepare the request
                request.SetupGet(x => x.HttpMethod).Returns(verb.ToString());
                request.SetupGet(x => x.ApplicationPath).Returns("/");
                request.SetupGet(x => x.FilePath).Returns(string.Empty);
                request.SetupGet(x => x.Url).Returns(new Uri(baseUrl, UriKind.RelativeOrAbsolute));
                request.SetupGet(x => x.Headers).Returns(headers);
                request.SetupGet(x => x.ServerVariables).Returns(serverVariables);
                request.SetupGet(x => x.QueryString).Returns(queryString);
                request.SetupGet(x => x.Form).Returns(forms);
                request.SetupGet(x => x.Cookies).Returns(cookiesCollection);
                request.SetupGet(x => x.RequestContext).Returns(requestContext.Object);

                // and we setup the reposnse, including the output
                response.SetupGet(x => x.Cookies).Returns(cookiesCollection);
                response.Setup(x => x.ApplyAppPathModifier(It.IsAny<string>())).Returns<string>((s) => s);
                using (var memoryStream = new MemoryStream())
                {
                    response.SetupGet(x => x.Output).Returns(new StreamWriter(memoryStream));
                }
            }

            // PART 2 - Generate the HttContext
            var httpContext = new HttpContext(
                new HttpRequest(
                    request.Object.FilePath,
                    request.Object.Url.AbsoluteUri,
                    request.Object.Url.Query.TrimStart('?')),
                new HttpResponse(response.Object.Output));

            // organizing route... now that we had to create
            // a new httpContext, a lot has to be set back to it
            var originalRoute = requestContext.Object;
            httpContext.Request.RequestContext.RouteData.Route = originalRoute.RouteData.Route;
            httpContext.Request.RequestContext.RouteData.RouteHandler = originalRoute.RouteData.RouteHandler = originalRoute.RouteData.RouteHandler;

            foreach (var routeDataValue in contextRouteData.Values)
            {
                httpContext.Request.RequestContext.RouteData.Values.Add(
                    routeDataValue.Key,
                    routeDataValue.Value);
            }

            foreach (var item in originalRoute.RouteData.DataTokens)
            {
                httpContext.Request.RequestContext.RouteData.DataTokens.Add(
                    item.Key,
                    item.Value);
            }

            SessionStateUtility.AddHttpSessionStateToContext(
                httpContext,
                session);

            // PART 3 - Generates HttpContextBase from HttpContext
            var wrapper = new HttpContextWrapper(httpContext);
            var context = (HttpContextBase)wrapper;

            // PART 4 - PREPARING APPLICATION
            var mvcApplication = new MvcApplication();
            mvcApplication.RegisterViewEngines(ViewEngines.Engines);
            mvcApplication.RegisterGlobalFilters(GlobalFilters.Filters);
            mvcApplication.RegisterGlobalSettings();

            // load the stuff to the mocked context
            RequestContext = new RequestContext(context, contextRouteData);
            ControllerContext = new ControllerContext(context, contextRouteData, baseController);
            UrlHelper = new UrlHelper(RequestContext, routes);
            TraditionalHttpContext = httpContext;
        }
    }
}


